package com.googlecode.meta;

/*
 * @(#)CheckNamesProcessor.java	1.2 06/09/28
 *
 * Copyright (c) 2006 Sun Microsystems, Inc. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * -Redistribution of source code must retain the above copyright notice, this
 *  list of conditions and the following disclaimer.
 *
 * -Redistribution in binary form must reproduce the above copyright notice,
 *  this list of conditions and the following disclaimer in the documentation
 *  and/or other materials provided with the distribution.
 *
 * Neither the name of Sun Microsystems, Inc. or the names of contributors may
 * be used to endorse or promote products derived from this software without
 * specific prior written permission.
 *
 * This software is provided "AS IS," without a warranty of any kind. ALL
 * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING
 * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
 * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN")
 * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE
 * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
 * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST
 * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL,
 * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY
 * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE,
 * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
 *
 * You acknowledge that this software is not designed, licensed or intended
 * for use in the design, construction, operation or maintenance of any
 * nuclear facility.
 */

import java.util.List;
import java.util.Set;
import java.util.EnumSet;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.lang.model.type.*;
import javax.lang.model.util.*;
import static javax.lang.model.SourceVersion.*;
import static javax.lang.model.element.Modifier.*;
import static javax.lang.model.element.ElementKind.*;
import static javax.lang.model.type.TypeKind.*;
import static javax.lang.model.util.ElementFilter.*;
import static javax.tools.Diagnostic.Kind.*;

/**
 * A sample processor to check naming conventions are being followed.
 * 
 * <h3>How to run this processor from the command line</h3>
 * <ol>
 * <li>Compile this file; for example<br>
 * {@code javac -d procdir CheckNamesProcessor.java}
 * <li>Use {@code javac} to run the annotation processor on itself:<br>
 * {@code javac -processorpath procdir -processor CheckNamesProcessor -proc:only
 * CheckNamesProcessor.java}
 * </ol>
 * 
 * <h3>Another way to run this processor from the command line</h3>
 * <ol>
 * <li>Compile the processor as before
 * 
 * <li>Create a UTF-8 encoded text file named {@code
 * javax.annotation.processing.Processor} in the {@code META-INF/services}
 * directory. The contents of the file are a list of the binary names of the
 * concrete processor classes, one per line. This provider-configuration file is
 * used by {@linkplain java.util.ServiceLoader service-loader} style lookup.
 * 
 * <li>Create a {@code jar} file with the processor classes and {@code META-INF}
 * information.
 * 
 * <li>Such a {@code jar} file can now be used with the <i>discovery process</i>
 * without explicitly naming the processor to run:<br>
 * {@code javac -processorpath procdir -proc:only CheckNamesProcessor.java}
 * 
 * </ol>
 * 
 * For some notes on how to run an annotation processor inside NetBeans, see
 * http://wiki.java.net/bin/view/Netbeans/FaqApt.
 * 
 * <h3>Possible Enhancements</h3>
 * <ul>
 * 
 * <li>Support an annotation processor option to control checking exported API
 * elements ({@code public} and {@code protected} ones) or all elements
 * 
 * <li>Print out warnings that are more informative
 * 
 * <li>Return a true/false status if any warnings were printed or compute and
 * return name warning count
 * 
 * <li>Implement checks of package names
 * 
 * <li>Use the Tree API, com.sun.source, to examine names within method bodies
 * 
 * <li>Define an annotation type whose presence can indicate a different naming
 * convention is being followed
 * 
 * <li>Implement customized checks on elements in chosen packages
 * 
 * </ul>
 * 
 * @author Joseph D. Darcy
 */
@SupportedAnnotationTypes("*")
// Process (check) everything
public class CheckNamesProcessor extends AbstractProcessor {
	private NameChecker nameChecker;
	private final List<MethodMetadata> methodMetadata;

	public CheckNamesProcessor(List<MethodMetadata> methodMetadata) {
		this.methodMetadata = methodMetadata;
	}

	/**
	 * Check that the names of the root elements (and their enclosed elements)
	 * follow the appropriate naming conventions. This processor examines all
	 * files regardless of whether or not annotations are present; no new source
	 * or class files are generated.
	 * 
	 * <p>
	 * Processors that actually process specific annotations should <em>not</em>
	 * report supporting {@code *}*}*}; this could cause performance degradations
	 * and other undesirable outcomes.
	 */
	@Override
	public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
		log("process");
		if (!roundEnv.processingOver()) {
			for (Element element : roundEnv.getRootElements())
				nameChecker.checkNames(element);
		}

		return false; // Allow other processors to examine files too.
	}

	private static void log(String msg) {
		System.out.println(msg);

	}

	@Override
	public void init(ProcessingEnvironment processingEnv) {
		super.init(processingEnv);
		nameChecker = new NameChecker(processingEnv, methodMetadata);
	}

	@Override
	public SourceVersion getSupportedSourceVersion() {
		/*
		 * Return latest source version instead of a fixed version like
		 * RELEASE_6. To return a fixed version, this class could be annotated
		 * with a SupportedSourceVersion annotation.
		 * 
		 * Warnings will be issued if any unknown language constructs are
		 * encountered.
		 */
		return SourceVersion.latest();
	}

	/**
	 * Provide checks that an element and its enclosed elements follow the usual
	 * naming conventions.
	 * 
	 * <p>
	 * Conventions from JLSv3 section 6.8:
	 * 
	 * <ul>
	 * <li>Classes and interfaces: camel case, first letter is uppercase
	 * <li>Methods: camel case, first letter is lowercase
	 * <li>Type variables: one uppercase letter
	 * <li>Fields
	 * <ul>
	 * <li>non-final: camel case, initial lowercase
	 * <li>constant: uppercase separated by underscores
	 * </ul>
	 * <li>Packages: checks left as exercise for the reader, see JLSv3 section
	 * 7.7
	 * </ul>
	 */
	private static class NameChecker {
		private final Messager messager;
		private final Types typeUtils;

		NameCheckScanner nameCheckScanner;

		NameChecker(ProcessingEnvironment processsingEnv, List<MethodMetadata> methodMetadata) {
			this.messager = processsingEnv.getMessager();
			this.typeUtils = processsingEnv.getTypeUtils();
			this.nameCheckScanner = new NameCheckScanner(methodMetadata);
		}

		/**
		 * If the name of the argument or its enclosed elements violates the
		 * naming conventions, report a warning.
		 */
		public void checkNames(Element element) {
			log("checkNames: " + element);
			// Implement name checks with a visitor, but expose that
			// functionality through this method instead.
			nameCheckScanner.scan(element);
		}

		/**
		 * Visitor to implement name checks.
		 */
		private class NameCheckScanner extends ElementScanner6<Void, Void> {
			// The visitor could be enhanced to return true/false if
			// there were warnings reported or a count of the number
			// of warnings. This could be facilitated by using
			// Boolean or Integer instead of Void for the actual type
			// arguments. In more detail, one way to tally the number
			// of warnings would be for each method to return the sum
			// of the warnings it and the methods it called issued, a
			// bottom-up computation. In that case, the first type
			// argument would be Integer and the second type argument
			// would still be Void. Alternatively, the current count
			// could be passed along in Integer parameter p and each
			// method could return the Integer sum of p and the
			// warnings the method issued. Some computations are more
			// naturally expressed in one form instead of the other.
			// If greater control is needed over traversal order, a
			// SimpleElementVisitor can be extended instead of an
			// ElementScanner.

			private List<MethodMetadata> methodMetadata;

			public NameCheckScanner(List<MethodMetadata> methodMetadata) {
				this.methodMetadata = methodMetadata;
			}

			/**
			 * Check the name of a type and its enclosed elements and type
			 * parameters.
			 */
			@Override
			public Void visitType(TypeElement e, Void p) {
				log("visitType: " + e + ", Void: " + p);

				scan(e.getTypeParameters(), p); // Check the names of any type
				// parameters
				checkCamelCase(e, true); // Check the name of the class or
				// interface
				super.visitType(e, p); // Check the names of any enclosed
				// elements
				return null;
			}

			/**
			 * Check the name of an executable (method, constructor, etc.) and
			 * its type parameters.
			 */
			@Override
			public Void visitExecutable(ExecutableElement e, Void p) {
				log("visitExecutable: " + e + ", Void: " + p);
				
				MethodMetadata currentMethodMetadata = new MethodMetadata( e.getSimpleName().toString() );
				methodMetadata.add( currentMethodMetadata);
				
				List<? extends VariableElement> parameters = e.getParameters();
				for (VariableElement variableElement : parameters) {
					currentMethodMetadata.addArgumentMetadata( new ArgumentMetadata( variableElement.getSimpleName().toString() ) );
				}

				scan(e.getTypeParameters(), p); // Check the names of any type
												// parameters

				// Check the name of the executable
				if (e.getKind() == METHOD) {
					// Make sure that a method does not have the same
					// name as its class or interface.
					Name name = e.getSimpleName();
					if (name.contentEquals(e.getEnclosingElement().getSimpleName()))
						messager.printMessage(WARNING, "A method should not have the same name as its enclosing type, ``" + name
								+ "''.", e);
					checkCamelCase(e, false);
				}
				// else constructors and initializers don't have user-defined
				// names

				// At this point, could use the Tree API,
				// com.sun.source, to examine the names of entities
				// inside a method.
				super.visitExecutable(e, p);
				return null;
			}

			/**
			 * Check the name of a field, parameter, etc.
			 */
			@Override
			public Void visitVariable(VariableElement e, Void p) {
				log("visitVariable: " + e + ", Void: " + p);

				if (!checkForSerial(e)) { // serialVersionUID checks
					// Is the variable a constant?
					if (e.getKind() == ENUM_CONSTANT || e.getConstantValue() != null || heuristicallyConstant(e))
						checkAllCaps(e); // includes enum constants
					else
						checkCamelCase(e, false);
				}
				// A call to super can be elided with the current language
				// definition.
				// super.visitVariable(e, p);
				return null;
			}

			/**
			 * Check the name of a type parameter.
			 */
			@Override
			public Void visitTypeParameter(TypeParameterElement e, Void p) {
				log("visitTypeParameter: " + e + ", Void: " + p);

				checkAllCaps(e);
				// A call to super can be elided with the current language
				// definition.
				// super.visitTypeParameter(e, p);
				return null;
			}

			/**
			 * Check the name of a package.
			 */
			@Override
			public Void visitPackage(PackageElement e, Void p) {
				log("visitPackage: " + e + ", Void: " + p);

				/*
				 * Implementing the checks of package names is left as an
				 * exercise for the reader, see JLSv3 section 7.7 for
				 * conventions.
				 */

				// Whether or not this method should call
				// super.visitPackage, to visit the packages enclosed
				// elements, is a design decision based on what a
				// PackageElemement is used to mean in this context.
				// A PackageElement can represent a whole package, so
				// it can provide a concise way to indicate many
				// user-defined types should be visited. However, a
				// PackageElement can also represent a
				// package-info.java file, as would be in the case if
				// the PackageElement came from
				// RoundEnvironment.getRootElements. In that case,
				// the package-info file and other files in that
				// package could be passed in. Therefore, without
				// further checks, types in a package could be visited
				// more than once if a package's elements were visited
				// too.
				return null;
			}

			@Override
			public Void visitUnknown(Element e, Void p) {
				log("visitUnknown: " + e + ", Void: " + p);

				// This method will be called if a kind of element
				// added after JDK 6 is visited. Since as of this
				// writing the conventions for such constructs aren't
				// known, issue a warning.
				messager.printMessage(WARNING, "Unknown kind of element, " + e.getKind() + ", no name checking performed.", e);
				return null;
			}

			// All the name checking methods assume the examined names
			// are syntactically well-formed identifiers.

			/**
			 * Return {@code true} if this variable is a field named
			 * "serialVersionUID"; false otherwise. A true serialVersionUID of a
			 * class has type {@code long} and is static and final.
			 * 
			 * <p>
			 * To check that a Serializable class defines a proper
			 * serialVersionUID, run javac with -Xlint:serial.
			 * 
			 * @return true if this variable is a serialVersionUID field and
			 *         false otherwise
			 */
			private boolean checkForSerial(VariableElement e) {
				log("checkForSerial: " + e);

				// If a field is named "serialVersionUID" ...
				if (e.getKind() == FIELD && e.getSimpleName().contentEquals("serialVersionUID")) {
					// ... issue a warning if it does not act as a
					// serialVersionUID
					if (!(e.getModifiers().containsAll(EnumSet.of(STATIC, FINAL))
							&& typeUtils.isSameType(e.asType(), typeUtils.getPrimitiveType(LONG)) && e.getEnclosingElement().getKind() == CLASS)) // could
						// check
						// that
						// class
						// implements
						// Serializable
						messager.printMessage(WARNING, "Field named ``serialVersionUID'' is not acting as such.", e);
					return true;
				}
				return false;
			}

			/**
			 * Using heuristics, return {@code true} is the variable should
			 * follow the naming conventions for constants and {@code false}
			 * otherwise. For example, the public static final fields ZERO, ONE,
			 * and TEN in java.math.BigDecimal are logically constants (and
			 * named as constants) even though BigDecimal values are not
			 * regarded as constants by the language specification. However,
			 * some final fields may not act as constants since the field may be
			 * a reference to a mutable object.
			 * 
			 * <p>
			 * These heuristics could be tweaked to provide better fidelity.
			 * 
			 * @return true if the current heuristics regard the variable as a
			 *         constant and false otherwise.
			 */
			private boolean heuristicallyConstant(VariableElement e) {
				log("heuristicallyConstant: " + e);

				// Fields declared in interfaces are logically
				// constants, JLSv3 section 9.3.
				if (e.getEnclosingElement().getKind() == INTERFACE)
					return true;
				else if (e.getKind() == FIELD && e.getModifiers().containsAll(EnumSet.of(PUBLIC, STATIC, FINAL)))
					return true;
				else {
					// A parameter declared final should not be named like
					// a constant, neither should exception parameters.
					return false;
				}
			}

			/**
			 * Print a warning if an element's simple name is not in camel case.
			 * If there are two adjacent uppercase characters, the name is
			 * considered to violate the camel case naming convention.
			 * 
			 * @param e
			 *            the element whose name will be checked
			 * @param initialCaps
			 *            whether or not the first character should be uppercase
			 */
			private void checkCamelCase(Element e, boolean initialCaps) {
				log("checkCamelCase: " + e);

				String name = e.getSimpleName().toString();
				boolean previousUpper = false;
				boolean conventional = true;
				int firstCodePoint = name.codePointAt(0);

				if (Character.isUpperCase(firstCodePoint)) {
					previousUpper = true;
					if (!initialCaps) {
						messager.printMessage(WARNING, "Name, ``" + name + "'', should start in lowercase.", e);
						return;
					}
				} else if (Character.isLowerCase(firstCodePoint)) {
					if (initialCaps) {
						messager.printMessage(WARNING, "Name, ``" + name + "'', should start in uppercase.", e);
						return;
					}
				} else
					// underscore, etc.
					conventional = false;

				if (conventional) {
					int cp = firstCodePoint;
					for (int i = Character.charCount(cp); i < name.length(); i += Character.charCount(cp)) {
						cp = name.codePointAt(i);
						if (Character.isUpperCase(cp)) {
							if (previousUpper) {
								conventional = false;
								break;
							}
							previousUpper = true;
						} else
							previousUpper = false;
					}
				}

				if (!conventional)
					messager.printMessage(WARNING, "Name, ``" + name + "'', should be in camel case.", e);
			}

			/**
			 * Print a warning if the element's name is not a sequence of
			 * uppercase letters separated by underscores ("_").
			 * 
			 * @param e
			 *            the element whose name will be checked
			 */
			private void checkAllCaps(Element e) {
				log("checkAllCaps: " + e);

				String name = e.getSimpleName().toString();
				log(e.toString());
				if (e.getKind() == TYPE_PARAMETER) { // Should be one character
					if (name.codePointCount(0, name.length()) > 1 ||
					// Assume names are non-empty
							!Character.isUpperCase(name.codePointAt(0)))
						messager.printMessage(WARNING, "A type variable's name,``" + name
								+ "'', should be a single uppercace character.", e);
				} else {
					boolean conventional = true;
					int firstCodePoint = name.codePointAt(0);

					// Starting with an underscore is not conventional
					if (!Character.isUpperCase(firstCodePoint))
						conventional = false;
					else {
						// Was the previous character an underscore?
						boolean previousUnderscore = false;
						int cp = firstCodePoint;
						for (int i = Character.charCount(cp); i < name.length(); i += Character.charCount(cp)) {
							cp = name.codePointAt(i);
							if (cp == (int) '_') {
								if (previousUnderscore) {
									conventional = false;
									break;
								}
								previousUnderscore = true;
							} else {
								previousUnderscore = false;
								if (!Character.isUpperCase(cp) && !Character.isDigit(cp)) {
									conventional = false;
									break;
								}
							}
						}
					}

					if (!conventional)
						messager.printMessage(WARNING, "A constant's name, ``" + name + "'', should be ALL_CAPS.", e);
				}
			}

		}
	}
}

/**
 * Lots of bad names. Don't write code like this!
 */
class BADLY_NAMED_CODE {
	enum colors {
		red, blue, green;
	}

	// Don't start the name of a constant with an underscore
	static final int _FORTY_TWO = 42;

	// Non-constants shouldn't use ALL_CAPS
	public static int NOT_A_CONSTANT = _FORTY_TWO;

	// *Not* a serialVersionUID
	private static final int serialVersionUID = _FORTY_TWO;

	// Not a constructor
	protected void BADLY_NAMED_CODE() {
		return;
	}

	public void NOTcamelCASEmethodNAME() {
		return;
	}
}
